package org.jflame.commons.file;

import org.jflame.commons.util.ArrayHelper;
import org.jflame.commons.util.StringHelper;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Path;
import java.nio.file.spi.FileTypeDetector;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.lang3.ArrayUtils;

/**
 * 通过文件头的二进制标识(Magic Number)判断可能文件类型.
 * <ol>
 * <li>由于很多文件并没有特定标识或者不同类型文件具体相同标识所以返回的是可能的文件类型数组</li>
 * <li>只对常见类型作了判断</li>
 * <li>本类实现了系统FileTypeDetector接口,优先使用文件头检测,如果无法检测再使用文件扩展名</li>
 * </ol>
 * 
 * @author charles.zhang
 * @see java.nio.file.spi.FileTypeDetector
 * @see Files.probeContentType(Path)
 */
public class MyFileTypeDetector extends FileTypeDetector {

    @Override
    public String probeContentType(Path path) throws IOException {
        byte[] b = new byte[FileTypeHeader.MIN_HEADER_LENGTH];
        File tmpFile = path.toFile();
        try (RandomAccessFile raf = new RandomAccessFile(tmpFile, "r")) {
            raf.read(b);
            String[] types = detect(b);
            if (types != null && types.length > 0) {
                if (types.length == 1) {
                    return MimeTypes.getInstance()
                            .getMimetype(tmpFile.getName());
                } else {
                    // 有多个可能结果且文件扩展名在其中
                    String ext = FileHelper.getExtension(tmpFile.getName(), false);
                    if (StringHelper.isNotEmpty(ext) && ArrayHelper.contains(types, ext)) {
                        return MimeTypes.getInstance()
                                .getMimetypeByExt(ext);
                    }
                }
            }
        }
        return MimeTypes.getInstance()
                .getMimetype(tmpFile.getName());
    }

    /**
     * 检测输入流可能的文件类型
     * 
     * @param source 输入流,注意大部分InputStream的读取都是不可复用的,所以如果需要重复读取流应该转换为可重复读流如ByteArrayInputStream
     * @return 返回可能的文件类型扩展名,没有检测到返回null
     */
    public static String[] detect(InputStream source) {
        byte[] b = new byte[FileTypeHeader.MIN_HEADER_LENGTH];
        try {
            source.read(b);
            return detect(b);
        } catch (IOException e) {
            return null;
        }
    }

    /**
     * 检测文件可能的类型
     * 
     * @param file
     * @return
     * @throws IOException
     */
    public static String[] detect(File file) throws IOException {
        byte[] b = new byte[FileTypeHeader.MIN_HEADER_LENGTH];
        try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
            raf.read(b);
            String[] types = detect(b);
            if (types != null && types.length > 1) {
                String ext = FileHelper.getExtension(file.getName(), false);
                if (StringHelper.isNotEmpty(ext) && ArrayHelper.contains(types, ext)) {
                    return ArrayHelper.of(ext);
                }
            }
            return types;
        }
    }

    public static String[] detect(byte[] b) {
        if (b.length < 2) {
            return null;
        }
        for (FileTypeHeader fh : FileTypeHeader.FILE_TYPE_HEADERS) {
            if (fh.match(b)) {
                return fh.getPossibleFileTypes();
            }
        }
        return null;

    }

    public static boolean isJPEG(InputStream source) throws IOException {
        InputStream iis = source;

        if (!source.markSupported()) {
            throw new IllegalArgumentException("Input stream must support mark");
        }

        iis.mark(30);
        // If the first two bytes are a JPEG SOI marker, it's probably
        // a JPEG file. If they aren't, it definitely isn't a JPEG file.
        try {
            int byte1 = iis.read();
            int byte2 = iis.read();
            if ((byte1 == 0xFF) && (byte2 == 0xD8)) {
                return true;
            }
        } finally {
            iis.reset();
        }

        return false;
    }

    public static boolean isBMP(InputStream in) throws IOException {
        if (!in.markSupported()) {
            throw new IllegalArgumentException("Input stream must support mark");
        }
        byte[] b = new byte[2];
        try {
            in.mark(30);
            in.read(b);
        } finally {
            in.reset();
        }

        return (b[0] == 0x42) && (b[1] == 0x4d);
    }

    public static boolean isGIF(InputStream in) throws IOException {
        if (!in.markSupported()) {
            throw new IllegalArgumentException("Input stream must support mark");
        }
        byte[] b = new byte[6];
        try {
            in.mark(30);
            in.read(b);
        } finally {
            in.reset();
        }
        return b[0] == 'G' && b[1] == 'I' && b[2] == 'F' && b[3] == '8' && (b[4] == '7' || b[4] == '9') && b[5] == 'a';
    }

    public static boolean isPNG(InputStream in) throws IOException {
        if (!in.markSupported()) {
            throw new IllegalArgumentException("Input stream must support mark");
        }

        byte[] b = new byte[8];
        try {
            in.mark(30);
            in.read(b);
        } finally {
            in.reset();
        }

        return (b[0] == (byte) 137 && b[1] == (byte) 80 && b[2] == (byte) 78 && b[3] == (byte) 71 && b[4] == (byte) 13
                && b[5] == (byte) 10 && b[6] == (byte) 26 && b[7] == (byte) 10);
    }

    public static boolean isTIFF(InputStream in) throws IOException {
        if (!in.markSupported()) {
            throw new IllegalArgumentException("Input stream must support mark");
        }
        byte[] b = new byte[4];
        try {
            in.mark(30);
            in.read(b);
        } finally {
            in.reset();
        }

        return ((b[0] == (byte) 0x49 && b[1] == (byte) 0x49 && b[2] == (byte) 0x2a && b[3] == (byte) 0x00)
                || (b[0] == (byte) 0x4d && b[1] == (byte) 0x4d && b[2] == (byte) 0x00 && b[3] == (byte) 0x2a));
    }

    /* public static void main(String[] args) throws IOException {
        BufferedInputStream is = FileHelper.newBufferedInputStream("C:\\Users\\Administrator\\Pictures\\28.jpg");
    
        System.out.println(detectImage(is));
        System.out
                .println(detectImage(FileHelper.newBufferedInputStream("C:\\Users\\Administrator\\Pictures\\91.png")));
        System.out.println(
                detectImage(FileHelper.newBufferedInputStream("C:\\Users\\Administrator\\Pictures\\regex.gif")));
        System.out.println(detectImage(FileHelper.newBufferedInputStream(
                "C:\\Windows\\WinSxS\\amd64_microsoft-windows-wmpnss-service_31bf3856ad364e35_10.0.19041.1_none_b977d9566df127e9\\wmpnss_color32.bmp")));
        System.out.println(detectImage(
                FileHelper.newBufferedInputStream("E:\\documents\\Legendshop\\LegendShop文档\\Spring MVC 3.0实战指南.ppt")));
    }*/

    public static class FileTypeHeader {

        private static Set<FileTypeHeader> FILE_TYPE_HEADERS = new LinkedHashSet<>();
        public static int MIN_HEADER_LENGTH = 16;
        static {
            // 图片文件
            FILE_TYPE_HEADERS.add(new FileTypeHeader("jpg,jpeg", new byte[] { (byte) 0xFF,(byte) 0xD8 }));
            FILE_TYPE_HEADERS
                    .add(new FileTypeHeader("png", new byte[] { (byte) 0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("gif", new byte[] { 0x47,0x49,0x46,0x38,0x37,0x61 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("gif", new byte[] { 0x47,0x49,0x46,0x38,0x39,0x61 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("bmp", new byte[] { 0x42,0x4D }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("ico", new byte[] { 0x00,0x00,0x01,0x00 }));
            FILE_TYPE_HEADERS
                    .add(new FileTypeHeader("cr2", new byte[] { 0x49,0x49,0x2A,0x00,0x10,0x00,0x00,0x00,0x43,0x52 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("tiff", new byte[] { 0x49,0x49,0x2A,0x00 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("tiff", new byte[] { 0x49,0x20,0x49 }));

            byte[] commonheaders1 = new byte[] { 0x52,0x49,0x46,0x46 };
            FILE_TYPE_HEADERS.add(new FileTypeHeader("webp", 0, commonheaders1, 9, new byte[] { 0x57,0x45,0x42,0x50 }));
            // 音视频文件
            FILE_TYPE_HEADERS.add(new FileTypeHeader("avi", 0, commonheaders1, 9,
                    new byte[] { 0x41,0x56,0x49,0x20,0x4C,0x49,0x53,0x54 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("wav", 0, commonheaders1, 9,
                    new byte[] { 0x57,0x41,0x56,0x45,0x66,0x6D,0x74,0x20 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mp3", new byte[] { 0x49,0x44,0x33 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mp3", new byte[] { (byte) 0xFF,(byte) 0xFB }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mp3", new byte[] { (byte) 0xFF,(byte) 0xF3 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mp3", new byte[] { (byte) 0xFF,(byte) 0xF2 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mp4", 4, new byte[] { 0x66,0x74,0x79,0x70,0x4d,0x53,0x4e,0x56 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mp4", 4, new byte[] { 0x66,0x74,0x79,0x70,0x69,0x73,0x6f,0x6d }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("m4v", 4, new byte[] { 0x66,0x74,0x79,0x70,0x4d,0x34,0x56,0x20 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("m4v", 4, new byte[] { 0x66,0x74,0x79,0x70,0x6d,0x70,0x34,0x32 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("3gp", 4, new byte[] { 0x66,0x74,0x79,0x70,0x33,0x67,0x70 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("rm,rmvb", new byte[] { 0x2E,0x52,0x4D,0x46 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mov", 4, new byte[] { 0x66,0x74,0x79,0x70,0x71,0x74,0x20,0x20 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mov", 4, new byte[] { 0x6D,0x6F,0x6F,0x76 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mov", 4, new byte[] { 0x6D,0x64,0x71,0x74 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("mov", 4, new byte[] { 0x66,0x72,0x65,0x65 }));
            FILE_TYPE_HEADERS.add(new MpgTypeHeader());

            FILE_TYPE_HEADERS.add(new FileTypeHeader("swf", new byte[] { 0x43,0x57,0x53 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("swf", new byte[] { 0x5A,0x57,0x53 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("asf,wma,wmv", new byte[] { 0x30,0x26,(byte) 0xB2,0x75,(byte) 0x8E,
                    0x66,(byte) 0xCF,0x11,(byte) 0xA6,(byte) 0xD9,0x00,(byte) 0xAA,0x00,0x62,(byte) 0xCE,0x6C }));
            // 常见文档
            FILE_TYPE_HEADERS.add(new FileTypeHeader("pdf", new byte[] { 0x25,0x50,0x44,0x46 }));//
            FILE_TYPE_HEADERS.add(new FileTypeHeader("doc", new byte[] { (byte) 0xDB,(byte) 0xA5,0x2D,0x00 }));// word2.0
            FILE_TYPE_HEADERS.add(new FileTypeHeader("doc,dot,pps,ppt,xla,xls",
                    new byte[] { (byte) 0xD0,(byte) 0xCF,0x11,(byte) 0xE0,(byte) 0xA1,(byte) 0xB1,0x1A,(byte) 0xE1 }));
            FILE_TYPE_HEADERS
                    .add(new FileTypeHeader("docx,pptx,xlsx", new byte[] { 0x50,0x4B,0x03,0x04,0x14,0x00,0x06,0x00 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("epub", new byte[] { 0x50,0x4B,0x03,0x04,0x0A,0x00,0x02,0x00 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("zip",
                    new byte[] { 0x50,0x4B,0x03,0x04,0x14,0x00,0x01,0x00,0x63,0x00,0x00,0x00,0x00,0x00 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("zip", new byte[] { 0x50,0x4B,0x05,0x06 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("zip", new byte[] { 0x50,0x4B,0x07,0x08 }));
            FILE_TYPE_HEADERS
                    .add(new FileTypeHeader("jar", new byte[] { 0x50,0x4B,0x03,0x04,0x14,0x00,0x08,0x00,0x08,0x00 }));
            FILE_TYPE_HEADERS
                    .add(new FileTypeHeader("docx,pptx,xlsx,zip,jar,tar,apk", new byte[] { 0x50,0x4B,0x03,0x04 }));
            // 压缩文件
            FILE_TYPE_HEADERS.add(new FileTypeHeader("rar", new byte[] { 0x52,0x61,0x72,0x21,0x1A,0x07,0x01,0x00 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("rar", new byte[] { 0x52,0x61,0x72,0x21,0x1A,0x07,0x00 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("gz,tgz,gzip", new byte[] { 0x1F,(byte) 0x8B,0x08 }));
            FILE_TYPE_HEADERS.add(new FileTypeHeader("7z", new byte[] { 0x37,0x7A,(byte) 0xBC,(byte) 0xAF,0x27,0x1C }));

        }

        private String[] possibleFileTypes;
        private int offset;
        private byte[] magicNumber;
        private int subOffset;
        private byte[] subMagicNumber;

        public FileTypeHeader() {
        }

        public FileTypeHeader(final String _fileExtension, final byte[] _magicNumber) {
            this(_fileExtension, 0, _magicNumber);
        }

        public FileTypeHeader(final String _fileExtension, int _offset, final byte[] _magicNumber) {
            if (_fileExtension.indexOf(',') > 0) {
                possibleFileTypes = _fileExtension.split(",");
            } else {
                possibleFileTypes = new String[] { _fileExtension };
            }
            offset = _offset;
            magicNumber = _magicNumber;
            if (magicNumber == null || magicNumber.length < 2) {
                throw new IllegalArgumentException("magicNumber's length must be more than 1");
            }
        }

        public FileTypeHeader(final String _fileExtension, int _offset, final byte[] _magicNumber,
                Integer _subHeaderOffset, final byte[] _subHeaderMagicNumber) {

            if (_fileExtension.indexOf(',') > 0) {
                possibleFileTypes = _fileExtension.split(",");
            } else {
                possibleFileTypes = new String[] { _fileExtension };
            }
            offset = _offset;
            magicNumber = _magicNumber;
            if (magicNumber == null || magicNumber.length < 2) {
                throw new IllegalArgumentException("magicNumber's length must be more than 1");
            }
            if (_subHeaderOffset != null) {
                subOffset = _subHeaderOffset;
            }
            subMagicNumber = _subHeaderMagicNumber;
        }

        public boolean match(byte[] fileHeaderBytes) {
            if (offset + magicNumber.length > fileHeaderBytes.length) {
                return false;
            }
            int j = offset;
            int i;
            for (i = 0; i < magicNumber.length; i++, j++) {
                if (magicNumber[i] != fileHeaderBytes[j]) {
                    return false;
                }
            }
            if (ArrayUtils.isNotEmpty(subMagicNumber)) {
                if (subOffset > offset && subOffset + subMagicNumber.length >= fileHeaderBytes.length) {
                    j = subOffset;
                    for (i = 0; i < subMagicNumber.length; i++, j++) {
                        if (subMagicNumber[i] != fileHeaderBytes[j]) {
                            return false;
                        }
                    }
                } else {
                    return false;
                }
            }
            return true;
        }

        /**
         * 计算最少需要计算的byte长度
         * 
         * @return
         */
        public int getMinHeaderByteLength() {
            int len = offset + magicNumber.length;
            if (subMagicNumber != null) {
                len = len + subOffset + subMagicNumber.length;
            }
            return len;
        }

        public String[] getPossibleFileTypes() {
            return possibleFileTypes;
        }

        public String getPossibleFileType() {
            return possibleFileTypes != null && possibleFileTypes.length > 0 ? possibleFileTypes[0] : "";
        }

        public int getOffset() {
            return offset;
        }

        public byte[] getMagicNumber() {
            return magicNumber;
        }

        public void setPossibleFileTypes(String[] possibleFileTypes) {
            this.possibleFileTypes = possibleFileTypes;
        }

        public void setOffset(int offset) {
            this.offset = offset;
        }

        public void setMagicNumber(byte[] magicNumber) {
            this.magicNumber = magicNumber;
        }

        public int getSubOffset() {
            return subOffset;
        }

        public void setSubOffset(int subOffset) {
            this.subOffset = subOffset;
        }

        public byte[] getSubMagicNumber() {
            return subMagicNumber;
        }

        public void setSubMagicNumber(byte[] subMagicNumber) {
            this.subMagicNumber = subMagicNumber;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + Arrays.hashCode(magicNumber);
            result = prime * result + Arrays.hashCode(possibleFileTypes);
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            FileTypeHeader other = (FileTypeHeader) obj;
            return Arrays.equals(magicNumber, other.magicNumber)
                    && Arrays.equals(possibleFileTypes, other.possibleFileTypes);
        }

    }

    public static class MpgTypeHeader extends FileTypeHeader {

        public MpgTypeHeader() {
            super("mpg", new byte[] { 0x00,0x00,0x01,(byte) 0xb0 });

        }

        @Override
        public boolean match(byte[] fileHeaderBytes) {
            return fileHeaderBytes.length > 3 && Objects.equals(fileHeaderBytes[0], getMagicNumber()[0])
                    && Objects.equals(fileHeaderBytes[1], getMagicNumber()[1])
                    && Objects.equals(fileHeaderBytes[2], getMagicNumber()[2])
                    && (fileHeaderBytes[3] >= getMagicNumber()[3] && fileHeaderBytes[3] <= (byte) 0xbf);
        }

    }

}
